/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.drawee.drawable;

import java.util.Arrays;

import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;

import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.VisibleForTesting;

/**
 * A drawable that fades to the specific layer.
 * <p/>
 * <p> Arbitrary number of layers is supported. 5 Different fade methods are supported.
 * Once the transition starts we will animate layers in or out based on used fade method.
 * fadeInLayer fades in specified layer to full opacity.
 * fadeOutLayer fades out specified layer to zero opacity.
 * fadeOutAllLayers fades out all layers to zero opacity.
 * fadeToLayer fades in specified layer to full opacity, fades out all other layers to zero opacity.
 * fadeUpToLayer fades in all layers up to specified layer to full opacity and
 * fades out all other layers to zero opacity.
 */
public class FadeDrawable extends ArrayDrawable {

    /**
     * A transition is about to start.
     */
    @VisibleForTesting
    public static final int TRANSITION_STARTING = 0;

    /**
     * The transition has started and the animation is in progress.
     */
    @VisibleForTesting
    public static final int TRANSITION_RUNNING = 1;

    /**
     * No transition will be applied.
     */
    @VisibleForTesting
    public static final int TRANSITION_NONE = 2;

    /**
     * Layers.
     */
    private final Drawable[] mLayers;

    /**
     * The current state.
     */
    @VisibleForTesting
    int mTransitionState;
    @VisibleForTesting
    int mDurationMs;
    @VisibleForTesting
    long mStartTimeMs;
    @VisibleForTesting
    int[] mStartAlphas;
    @VisibleForTesting
    int[] mAlphas;
    @VisibleForTesting
    int mAlpha;

    /**
     * Determines whether to fade-out a layer to zero opacity (false) or to fade-in to
     * the full opacity (true)
     */
    @VisibleForTesting
    boolean[] mIsLayerOn;

    /**
     * When in batch mode, drawable won't invalidate self until batch mode finishes.
     */
    @VisibleForTesting
    int mPreventInvalidateCount;

    /**
     * Creates a new fade drawable.
     * The first layer is displayed with full opacity whereas all other layers are invisible.
     *
     * @param layers layers to fade between
     */
    public FadeDrawable(Drawable[] layers) {
        super(layers);
        Preconditions.checkState(layers.length >= 1, "At least one layer required!");
        mLayers = layers;
        mStartAlphas = new int[layers.length];
        mAlphas = new int[layers.length];
        mAlpha = 255;
        mIsLayerOn = new boolean[layers.length];
        mPreventInvalidateCount = 0;
        resetInternal();
    }

    @Override
    public void invalidateSelf() {
        if (mPreventInvalidateCount == 0) {
            super.invalidateSelf();
        }
    }

    /**
     * Begins the batch mode so that it doesn't invalidate self on every operation.
     */
    public void beginBatchMode() {
        mPreventInvalidateCount++;
    }

    /**
     * Ends the batch mode and invalidates.
     */
    public void endBatchMode() {
        mPreventInvalidateCount--;
        invalidateSelf();
    }

    /**
     * Sets the duration of the current transition in milliseconds.
     */
    public void setTransitionDuration(int durationMs) {
        mDurationMs = durationMs;
        // re-initialize transition if it's running
        if (mTransitionState == TRANSITION_RUNNING) {
            mTransitionState = TRANSITION_STARTING;
        }
    }

    /**
     * Gets the transition duration.
     *
     * @return transition duration in milliseconds.
     */
    public int getTransitionDuration() {
        return mDurationMs;
    }

    /**
     * Resets internal state to the initial state.
     */
    private void resetInternal() {
        mTransitionState = TRANSITION_NONE;
        Arrays.fill(mStartAlphas, 0);
        mStartAlphas[0] = 255;
        Arrays.fill(mAlphas, 0);
        mAlphas[0] = 255;
        Arrays.fill(mIsLayerOn, false);
        mIsLayerOn[0] = true;
    }

    /**
     * Resets to the initial state.
     */
    public void reset() {
        resetInternal();
        invalidateSelf();
    }

    /**
     * Starts fading in the specified layer.
     *
     * @param index the index of the layer to fade in.
     */
    public void fadeInLayer(int index) {
        mTransitionState = TRANSITION_STARTING;
        mIsLayerOn[index] = true;
        invalidateSelf();
    }

    /**
     * Starts fading out the specified layer.
     *
     * @param index the index of the layer to fade out.
     */
    public void fadeOutLayer(int index) {
        mTransitionState = TRANSITION_STARTING;
        mIsLayerOn[index] = false;
        invalidateSelf();
    }

    /**
     * Starts fading in all layers.
     */
    public void fadeInAllLayers() {
        mTransitionState = TRANSITION_STARTING;
        Arrays.fill(mIsLayerOn, true);
        invalidateSelf();
    }

    /**
     * Starts fading out all layers.
     */
    public void fadeOutAllLayers() {
        mTransitionState = TRANSITION_STARTING;
        Arrays.fill(mIsLayerOn, false);
        invalidateSelf();
    }

    /**
     * Starts fading to the specified layer.
     *
     * @param index the index of the layer to fade to
     */
    public void fadeToLayer(int index) {
        mTransitionState = TRANSITION_STARTING;
        Arrays.fill(mIsLayerOn, false);
        mIsLayerOn[index] = true;
        invalidateSelf();
    }

    /**
     * Starts fading up to the specified layer.
     * <p/>
     * Layers up to the specified layer inclusive will fade in, other layers will fade out.
     *
     * @param index the index of the layer to fade up to.
     */
    public void fadeUpToLayer(int index) {
        mTransitionState = TRANSITION_STARTING;
        Arrays.fill(mIsLayerOn, 0, index + 1, true);
        Arrays.fill(mIsLayerOn, index + 1, mLayers.length, false);
        invalidateSelf();
    }

    /**
     * Finishes transition immediately.
     */
    public void finishTransitionImmediately() {
        mTransitionState = TRANSITION_NONE;
        for (int i = 0; i < mLayers.length; i++) {
            mAlphas[i] = mIsLayerOn[i] ? 255 : 0;
        }
        invalidateSelf();
    }

    /**
     * Updates the current alphas based on the ratio of the elapsed time and duration.
     *
     * @param ratio
     * @return whether the all layers have reached their target opacity
     */
    private boolean updateAlphas(float ratio) {
        boolean done = true;
        for (int i = 0; i < mLayers.length; i++) {
            int dir = mIsLayerOn[i] ? +1 : -1;
            // determines alpha value and clamps it to [0, 255]
            mAlphas[i] = (int) (mStartAlphas[i] + dir * 255 * ratio);
            if (mAlphas[i] < 0) {
                mAlphas[i] = 0;
            }
            if (mAlphas[i] > 255) {
                mAlphas[i] = 255;
            }
            // determines whether the layer has reached its target opacity
            if (mIsLayerOn[i] && mAlphas[i] < 255) {
                done = false;
            }
            if (!mIsLayerOn[i] && mAlphas[i] > 0) {
                done = false;
            }
        }
        return done;
    }

    @Override
    public void draw(Canvas canvas) {
        boolean done = true;
        float ratio;

        switch (mTransitionState) {
            case TRANSITION_STARTING:
                // initialize start alphas and start time
                System.arraycopy(mAlphas, 0, mStartAlphas, 0, mLayers.length);
                mStartTimeMs = getCurrentTimeMs();
                // if the duration is 0, update alphas to the target opacities immediately
                ratio = (mDurationMs == 0) ? 1.0f : 0.0f;
                // if all the layers have reached their target opacity, transition is done
                done = updateAlphas(ratio);
                mTransitionState = done ? TRANSITION_NONE : TRANSITION_RUNNING;
                break;

            case TRANSITION_RUNNING:
                Preconditions.checkState(mDurationMs > 0);
                // determine ratio based on the elapsed time
                ratio = (float) (getCurrentTimeMs() - mStartTimeMs) / mDurationMs;
                // if all the layers have reached their target opacity, transition is done
                done = updateAlphas(ratio);
                mTransitionState = done ? TRANSITION_NONE : TRANSITION_RUNNING;
                break;

            case TRANSITION_NONE:
                // there is no transition in progress and mAlphas should be left as is.
                done = true;
                break;
        }

        for (int i = 0; i < mLayers.length; i++) {
            drawDrawableWithAlpha(canvas, mLayers[i], mAlphas[i] * mAlpha / 255);
        }

        if (!done) {
            invalidateSelf();
        }
    }

    private void drawDrawableWithAlpha(Canvas canvas, Drawable drawable, int alpha) {
        if (alpha > 0) {
            mPreventInvalidateCount++;
            drawable.mutate().setAlpha(alpha);
            mPreventInvalidateCount--;
            drawable.draw(canvas);
        }
    }

    @Override
    public void setAlpha(int alpha) {
        if (mAlpha != alpha) {
            mAlpha = alpha;
            invalidateSelf();
        }
    }

    public int getAlpha() {
        return mAlpha;
    }

    /**
     * Returns current time. Absolute reference is not important as only time deltas are used.
     * Extracting this to a separate method allows better testing.
     *
     * @return current time in milliseconds
     */
    protected long getCurrentTimeMs() {
        return SystemClock.uptimeMillis();
    }

    /**
     * Gets the transition state (STARTING, RUNNING, NONE).
     * Useful for testing purposes.
     *
     * @return transition state
     */
    @VisibleForTesting
    public int getTransitionState() {
        return mTransitionState;
    }

    public boolean isLayerOn(int index) {
        return mIsLayerOn[index];
    }

}
